#include "PyNpObject.h"

#include <vector>

#include <boost/python.hpp>
#include <boost/python/raw_function.hpp>
#include <boost/python/detail/api_placeholder.hpp>

#include <API/WebView.h>

namespace python = boost::python;
using namespace python;

#include <windows.h>

#include "boost_python_helpers/gil.hpp"

//----------------------------------------------------------------------------
//----------------------------------------------------------------------------
// NOTE : we're not supposed to call/run Javascript from another thread,
// a crash in V8 after a direct call to an NPN_... function from this source
// is probably due to this cause.
//
// That's the purpose of the various Javascript... functions, that relay NPN_...
// calls to the real thread.
//----------------------------------------------------------------------------
//----------------------------------------------------------------------------

#define NPN_CreateObject   Awesomium::WebBindings::createObject
//#define NPN_GetProperty    Awesomium::WebBindings::getProperty
#define NPN_GetStringIdentifier Awesomium::WebBindings::getStringIdentifier
//#define NPN_Invoke         Awesomium::WebBindings::invoke
//#define NPN_InvokeDefault  Awesomium::WebBindings::invokeDefault
#define NPN_ReleaseObject  Awesomium::WebBindings::releaseObject
#define NPN_ReleaseVariantValue Awesomium::WebBindings::releaseVariantValue
#define NPN_RetainObject   Awesomium::WebBindings::retainObject
#define NPN_UTF8FromIdentifier Awesomium::WebBindings::utf8FromIdentifier

#define NPN_MemAlloc Awesomium::WebBindings::memAlloc
#define NPN_MemFree Awesomium::WebBindings::memFree

namespace {

  bool JavascriptInvokeDefault(Awesomium::WebView* view, NPObject* object, const NPVariant* args, uint32_t count, NPVariant* result)
  {
    allow_threading_guard unlock;      // prevent deadlocks when having javascript calling python from core thread
    return Awesomium::WebBindings::invokeDefault(view, object, args, count, result);
  }
  
  bool JavascriptInvoke(Awesomium::WebView* view, NPObject* object, NPIdentifier methodName, const NPVariant* args, uint32_t count, NPVariant* result)
  {
    allow_threading_guard unlock;      // prevent deadlocks when having javascript calling python from core thread
    return Awesomium::WebBindings::invoke(view, object, methodName, args, count, result);
  }

  bool JavascriptGetProperty(Awesomium::WebView* view, NPObject* object, NPIdentifier propertyName, NPVariant* result)
  {
    allow_threading_guard unlock;      // prevent deadlocks when having javascript calling python from core thread
    return Awesomium::WebBindings::getProperty(view, object, propertyName, result);
  }

}

//----------------------------------------------------------------------------

namespace {

  const NPP NULL_NPP = NULL;

  std::string to_string(NPIdentifier npid)
  {
    std::string result;
    
    NPUTF8 *name_utf8 = NPN_UTF8FromIdentifier(npid);
    result = name_utf8;
    NPN_MemFree(name_utf8);
    
    return result;
  }

  bool has_named_object(python::object const& object, char const* name, bool search_callable)
  {
    bool has_named_attr = PyObject_HasAttrString(object.ptr(), name);

    if (!has_named_attr)
    {
      return false;
    }
    else
    {
      bool result = false;

      try
      {
        python::object const& named_object = object.attr(name);

        bool is_callable_named_object = PyObject_HasAttrString(named_object.ptr(), "__call__");

        result = !(search_callable ^ is_callable_named_object);
      }
      catch (boost::python::error_already_set const&)
      {
      }

      return result;
    }
  }

  python::object find_named_object(python::object const& object, char const* name)
  {
    python::object result;

    try
    {
      result = object.attr(name);
    }
    catch (boost::python::error_already_set const&)
    {
    }
    
    return result;
  }

  void convert(Awesomium::WebView* view, python::object const& object, NPVariant& v);

  //----------------------------------------------------------------------------

  class JsPyObject
  {
    std::string m_methodname;
    Awesomium::WebView* m_view;
    NPObject* m_call_result;
    NPObject* m_called_object;

  public:
    JsPyObject(NPObject* callresult, Awesomium::WebView* view)
    : m_call_result(NULL)
    , m_view(NULL)
    , m_called_object(NULL)
    {
      reset( callresult, view, NULL, "" );
    }

    JsPyObject(NPObject* callresult, NPObject* called_object, Awesomium::WebView* view, std::string const& method_name)
    : m_call_result(NULL)
    , m_view(NULL)
    , m_called_object(NULL)
    {
      reset( callresult, view, called_object, method_name );
    }

    JsPyObject(JsPyObject const& sc)
    : m_call_result(NULL)
    , m_view(NULL)
    , m_called_object(NULL)
    {
      reset( sc.m_call_result, sc.m_view, sc.m_called_object, sc.m_methodname );
    }

    ~JsPyObject()
    {
      reset( NULL, NULL, NULL, "" );
    }

    JsPyObject& operator=(JsPyObject const& src)
    {
      if (&src != this)
      {
        reset( src.m_call_result, src.m_view, src.m_called_object, src.m_methodname );
      }
      return (*this);
    }

    void reset(NPObject* call_result, Awesomium::WebView* view, NPObject* called_object, std::string const& method_name)
    {
      if (m_call_result != NULL)
        NPN_ReleaseObject(m_call_result);
      if (m_called_object != NULL)
        NPN_ReleaseObject(m_called_object);
      
      m_call_result = call_result;
      m_view = view;
      m_methodname = method_name;
      m_called_object = called_object;
      
      if (m_call_result != NULL)
        NPN_RetainObject(m_call_result);
      if (m_called_object != NULL)
        NPN_RetainObject(m_called_object);
    }

    python::object raw_call( python::tuple pyArgs, python::dict kw )
    {
      NPObject* object_to_invoke = (m_called_object == NULL) ? m_call_result : m_called_object;

      python::object pyResult;
      if (object_to_invoke != NULL)
      {
        NPVariant result;

        unsigned num_args = len( pyArgs );

        if (num_args == 0)
        {
          if (m_called_object == NULL)
          {
            JavascriptInvokeDefault(m_view, object_to_invoke, NULL, 0, &result);
          }
          else
          {
            NPIdentifier methodName = NPN_GetStringIdentifier(m_methodname.c_str());
            JavascriptInvoke(m_view, object_to_invoke, methodName, NULL, 0, &result);
          }
        }
        else
        {
          std::vector<NPVariant> args( num_args );
          for (unsigned i=0; i<num_args; i++)
            convert(m_view, pyArgs[i], args[i]);

          if (m_called_object == NULL)
          {
            JavascriptInvokeDefault(m_view, object_to_invoke, &args[0], num_args, &result);
          }
          else
          {
            NPIdentifier methodName = NPN_GetStringIdentifier(m_methodname.c_str());
            JavascriptInvoke(m_view, object_to_invoke, methodName, &args[0], num_args, &result);
          }

          for (unsigned i=0; i<num_args; i++)
            NPN_ReleaseVariantValue(&args[i]);
        }
        
        pyResult = convert( m_view, result );

        NPN_ReleaseVariantValue(&result);
      }
      return pyResult;
    }

    python::object getattr( std::string const& name )
    {
      python::object pyResult;
      if (m_call_result != NULL)
      {
        NPVariant result;

        NPIdentifier propName = NPN_GetStringIdentifier(name.c_str());

        JavascriptGetProperty(m_view, m_call_result, propName, &result);

        // here we have to have to handle the case of 'getattr' for callable methods ::
        // if we have a javascript class 'A' with method 'a', we have to store the 'A' instance and the 'a' name.

        pyResult = convert( m_view, result, m_call_result, name );

        NPN_ReleaseVariantValue(&result);
      }
      return pyResult;
    }
  };

} // namespace {

  //----------------------------------------------------------------------------

  python::object convert(Awesomium::WebView* view, NPVariant const& v, NPObject* called_object/*=NULL*/, std::string const& method_name/*=""*/)
  {
    python::object result;

    if (NPVARIANT_IS_VOID(v))
    {
    }
    else
    if (NPVARIANT_IS_NULL(v))
    {
      result = object( detail::borrowed_reference(Py_None) );
    }
    else
    if (NPVARIANT_IS_BOOLEAN(v))
    {
      result = object( bool(NPVARIANT_TO_BOOLEAN(v)) );
    }
    else
    if (NPVARIANT_IS_INT32(v))
    {
      int value = NPVARIANT_TO_INT32(v);
      result = object( value );
    }
    else
    if (NPVARIANT_IS_DOUBLE(v))
    {
      result = object( NPVARIANT_TO_DOUBLE(v) );
    }
    else
    if (NPVARIANT_IS_STRING(v))
    {
      PyObject* pystring = PyUnicode_DecodeUTF8( NPVARIANT_TO_STRING(v).UTF8Characters, NPVARIANT_TO_STRING(v).UTF8Length, NULL );
      if (pystring == NULL)
      {
        boost::python::throw_error_already_set();
      }
      else
      {
        result = object( handle<>(pystring) );
      }
    }
    else
    if (NPVARIANT_IS_OBJECT(v))
    {
      NPObject* npobj = NPVARIANT_TO_OBJECT(v);

      if (npobj->_class == GET_NPOBJECT_CLASS(PyNpObject))
      {
        PyNpObject* pythonObj = static_cast<PyNpObject*>( npobj );
        result = pythonObj->getPythonObject();
      }
      else
      {
        if (view == NULL)
        {
          // prevent javascript object conversion, as we don't have the connection with the javascript source thread
          // so we can't invoke it.
          result = object("<orphan javascript object>");
        }
        else
        {
          if (called_object == NULL)
          {
            result = object( JsPyObject(npobj, view) );
          }
          else
          {
            result = object( JsPyObject(npobj, called_object, view, method_name) );
          }
        }
      }
    }

    return result;
  }

  //----------------------------------------------------------------------------

namespace {

  void convert(Awesomium::WebView* view, python::object const& object, NPVariant& v)
  {
    VOID_TO_NPVARIANT(v);

    if (object.ptr() == Py_None)
    {
      NULL_TO_NPVARIANT(v);
      return;
    }
    {
      extract<double> x(object);
      if (x.check())
      {
        DOUBLE_TO_NPVARIANT( double(x), v );
        return;
      }
    }
    {
      extract<int> x(object);
      if (x.check())
      {
        INT32_TO_NPVARIANT( int(x), v );
        return;
      }
    }
    {
      extract<bool> x(object);
      if (x.check())
      {
        BOOLEAN_TO_NPVARIANT( bool(x), v );
        return;
      }
    }
    {
      if (PyString_Check(object.ptr()))
      {
        char* py_internal_buffer = NULL;
        Py_ssize_t buffer_length = 0;
        int result = PyString_AsStringAndSize(object.ptr(), &py_internal_buffer, &buffer_length);

        if (result == -1)
        {
          boost::python::throw_error_already_set();
        }
        else
        {
          NPUTF8* string_data = static_cast<NPUTF8*>( NPN_MemAlloc(buffer_length) );

          memcpy( string_data, py_internal_buffer, buffer_length );
          
          STRINGN_TO_NPVARIANT(string_data, buffer_length, v);
        }
        return;
      }
    }
    {
      if (PyUnicode_Check(object.ptr()))
      {
        PyObject* pyutf8 = PyUnicode_AsUTF8String(object.ptr());
        if (pyutf8 == NULL)
        {
          boost::python::throw_error_already_set();
        }
        else
        {
          python::object utf8 = python::object( handle<>(pyutf8) );

          char* py_internal_buffer = NULL;
          Py_ssize_t buffer_length = 0;
          int result = PyString_AsStringAndSize(utf8.ptr(), &py_internal_buffer, &buffer_length);

          if (result == -1)
          {
            boost::python::throw_error_already_set();
          }
          else
          {
            NPUTF8* string_data = static_cast<NPUTF8*>( NPN_MemAlloc(buffer_length) );

            memcpy( string_data, py_internal_buffer, buffer_length );
            
            STRINGN_TO_NPVARIANT(string_data, buffer_length, v);
          }
          return;
        }
      }
    }
    {
      NPObject* result_object = NPN_CreateObject(NULL_NPP, GET_NPOBJECT_CLASS(PyNpObject));

      PyNpObject* pythonScripting = static_cast<PyNpObject*>( result_object );
      pythonScripting->setPythonObject( object );

      OBJECT_TO_NPVARIANT(result_object, v);
    }
  }

};

//----------------------------------------------------------------------------

python::object call_raw_member(python::tuple raw, python::dict kw)
{
  return extract<JsPyObject&>(raw[0])().raw_call( tuple(raw.slice(1, _)), kw);
}

BOOST_PYTHON_MODULE( js_to_python )
{
  class_<JsPyObject>("JsPyObject", no_init)
    .def("__call__", raw_function(call_raw_member))
    .def("__getattr__", &JsPyObject::getattr)
    ;
}

namespace 
{
  NPObject *PythonScriptablePluginObject_Factory(NPP view_as_NPP, NPClass *aClass)
  {
    return new PyNpObject( reinterpret_cast<Awesomium::WebView*>(view_as_NPP) );
  }
};

DEFINE_NPOBJECT_CLASS( PyNpObject, PythonScriptablePluginObject_Factory );

PyNpObject::PyNpObject(Awesomium::WebView* view)
: CppNpObject(view)
{
}

PyNpObject::~PyNpObject()
{
  lock_gil python_gil;
  m_object = object();
}

//----------------------------------------------------------------------------

bool PyNpObject::HasMethod(NPIdentifier name_id)
{
  lock_gil python_gil;

  std::string name = to_string(name_id);

  if (name == "valueOf")
    return true;

  if (name == "Function" || name == "Object")
    return false;

  bool result = has_named_object(m_object, name.c_str(), true);
  return result;
}

//----------------------------------------------------------------------------

bool PyNpObject::Invoke(NPIdentifier name_id, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
  lock_gil python_gil;

  std::string name = to_string(name_id);

  try
  {
    if (name == "valueOf")
    {
      convert(m_view, m_object, *result);
      return true;
    }
    else
    {
      python::object _method = find_named_object(m_object, name.c_str());
      
      python::list pyArgList;
      for (unsigned i=0; i<argCount; i++)
        pyArgList.append( convert(m_view, args[i]) );

      python::tuple pyArgTuple( pyArgList ); 

      object pyResult( handle<>( PyEval_CallObject(_method.ptr(), pyArgTuple.ptr()) ) );

      convert(m_view, pyResult, *result);

      return true;
    }
  }
  catch (boost::python::error_already_set)
  {
    // print all other errors to stderr
    PyErr_Print();
  }
  catch (...)
  {
  }
  return false;
}

//----------------------------------------------------------------------------

bool PyNpObject::InvokeDefault(const NPVariant *args, uint32_t argCount, NPVariant *result)
{
  lock_gil python_gil;

  try
  {   
    python::list pyArgList;
    for (unsigned i=0; i<argCount; i++)
      pyArgList.append( convert(m_view, args[i]) );

    python::tuple pyArgTuple( pyArgList ); 

    object pyResult( handle<>( PyEval_CallObject(m_object.ptr(), pyArgTuple.ptr()) ) );

    convert(m_view, pyResult, *result);

    return true;
  }
  catch (boost::python::error_already_set)
  {
    // print all other errors to stderr
    PyErr_Print();
  }
  catch (...)
  {
  }
  return false;
}

//----------------------------------------------------------------------------

bool PyNpObject::HasProperty(NPIdentifier name_id)
{
  lock_gil python_gil;

  std::string name = to_string(name_id);

  if (name == "valueOf")
    return true;

  if (name == "Function" || name == "Object")
    return false;

  bool result = has_named_object(m_object, name.c_str(), false);
  return result;
}

//----------------------------------------------------------------------------

bool PyNpObject::GetProperty(NPIdentifier name_id, NPVariant *result)
{
  lock_gil python_gil;

  std::string name = to_string(name_id);

  try
  {
    if (name == "valueOf")
    {
      convert(m_view, m_object, *result);
      return true;
    }
    else
    {
      python::object _property = find_named_object(m_object, name.c_str());
      
      convert(m_view, _property, *result);

      return true;
    }
  }
  catch (boost::python::error_already_set)
  {
    // print all other errors to stderr
    PyErr_Print();
  }
  catch (...)
  {
  }
  return false;
}

//----------------------------------------------------------------------------

bool PyNpObject::SetProperty(NPIdentifier name_id, const NPVariant *value)
{
  lock_gil python_gil;

  std::string name = to_string(name_id);

  try
  {
    m_object.attr( name.c_str() ) = convert(m_view, *value);
    return true;
  }
  catch (boost::python::error_already_set)
  {
    // print all other errors to stderr
    PyErr_Print();
  }
  catch (...)
  {
  }
  return false;
}
